﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.ComponentModel;
using System.Runtime.Serialization;

namespace WcfFlashRemoting.ServiceModel
{
    class WebTypeData
    {
        private string contractName;
        private delegate void SetValueDelegate(object target, object value, Dictionary<string, Type> knownTypes);
        private delegate object GetValueDelegate(object target);
        private Dictionary<string, Type> expectedTypes = new Dictionary<string, Type>();
        private Dictionary<string, SetValueDelegate> namedSetProperties = new Dictionary<string, SetValueDelegate>();
        private Dictionary<string, GetValueDelegate> namedGetProperties = new Dictionary<string, GetValueDelegate>();
        private List<string> requiredProperties = new List<string>();
        private Type type;
        private Dictionary<string, Type> typeKnownTypes;

        public WebTypeData(Type t)
        {
            this.type = t;
            typeKnownTypes = type.GetCustomAttributes(typeof(KnownTypeAttribute), false)
                .Select(attr => (attr as KnownTypeAttribute).Type) // get the type from the attribute
                .Where(knownType => t.IsAssignableFrom(knownType)) // make sure the type is actually derived from our base class
                .ToDictionary(knownType => TypeHelper.GetContractName(knownType)); // converts it to a dictionary for quick lookup
            this.contractName = TypeHelper.GetContractName(t);
            IEnumerable<MemberInfo> members = t.GetMembers(BindingFlags.Instance | BindingFlags.Public)
                .Where(mi => mi is FieldInfo || mi is PropertyInfo)
                .Where(mi => mi.GetCustomAttributes(typeof(System.Runtime.Serialization.DataMemberAttribute), true).Length > 0);
            foreach (MemberInfo mi in members)
            {
                System.Runtime.Serialization.DataMemberAttribute dataMemberAttr = (mi.GetCustomAttributes(typeof(System.Runtime.Serialization.DataMemberAttribute), true)[0] as System.Runtime.Serialization.DataMemberAttribute);
                string name = dataMemberAttr.Name ?? mi.Name;

                Type expectedType;
                SetValueDelegate setDelegate;
                GetValueDelegate getDelegate;
                if (mi is FieldInfo)
                {
                    FieldInfo fi = mi as FieldInfo;
                    setDelegate = (target, value, knownTypes) => fi.SetValue(target, TypeHelper.Instantiate(fi.FieldType, value, knownTypes));
                    getDelegate = fi.GetValue;
                    expectedType = fi.FieldType;
                }
                else
                {
                    PropertyInfo pi = mi as PropertyInfo;
                    setDelegate = (target, value, knownTypes) => pi.GetSetMethod().Invoke(target, new object[] { TypeHelper.Instantiate(pi.PropertyType, value, knownTypes) });
                    getDelegate = (target) => pi.GetGetMethod().Invoke(target, null);
                    expectedType = pi.PropertyType;
                }

                expectedTypes.Add(name, expectedType);
                namedSetProperties.Add(name, setDelegate);
                namedGetProperties.Add(name, getDelegate);
                if (dataMemberAttr.IsRequired)
                {
                    requiredProperties.Add(name);
                }
            }
        }

        public object Instantiate(object properties, Dictionary<string, Type> knownTypes)
        {
            List<string> remainingRequired = new List<string>(requiredProperties);
            string amfType = null;
            IDictionary<string, object> dict = null;
            if (properties is AmfObjects.NamedDictionary)
            {
                amfType = (properties as AmfObjects.NamedDictionary).TypeName;
                dict = (properties as AmfObjects.NamedDictionary).Properties;


                if ((string.IsNullOrEmpty(amfType) || amfType == "Object") && dict.ContainsKey("amfType"))
                {
                    amfType = dict["amfType"].ToString();
                }
                if (!string.IsNullOrEmpty(amfType) && amfType != contractName)
                {
                    // looks up another type because of the knownType info.
                    
                    Type t = knownTypes.ContainsKey(amfType) ? knownTypes[amfType] : null;
                    t = t ?? (typeKnownTypes.ContainsKey(amfType) ? typeKnownTypes[amfType] : null);
                    if (t == null)
                    {
                        throw new InvalidOperationException("Specified type '" + amfType + "' not found.");
                    }
                    else if (t != null && t != type)
                    {
                        if (type.IsAssignableFrom(t) && t.GetCustomAttributes(typeof(System.Runtime.Serialization.DataContractAttribute), false).Length > 0)
                        {
                            return TypeHelper.Instantiate(t, properties, knownTypes);
                        }
                        else
                        {
                            throw new InvalidOperationException("Specified type '" + amfType + "' is not assignable to type '" + t.AssemblyQualifiedName + "'.");
                        }
                    }
                }
            }

            dict = dict ?? (properties as IDictionary<string, object>);

            if (dict != null)
            {
                object result = System.Runtime.Serialization.FormatterServices.GetSafeUninitializedObject(type);
                foreach (KeyValuePair<string, object> kvp in dict)
                {
                    if (kvp.Key == "amfType")
                    {
                        continue;
                    }
                    else if (this.namedSetProperties.ContainsKey(kvp.Key))
                    {
                        try
                        {
                            this.namedSetProperties[kvp.Key](result, kvp.Value, knownTypes);
                            if (remainingRequired.Contains(kvp.Key))
                            {
                                remainingRequired.Remove(kvp.Key);
                            }
                        }
                        catch
                        {
                            throw new InvalidOperationException(string.Format("Unable to instantiate object type '{0}'. Could not set property '{1}', given value '{2}'.", type.Name, kvp.Key, kvp.Value.ToString()));
                        }
                    }
                    else 
                    {
                        StringBuilder sb = new StringBuilder();
                        sb.Append("Unable to instantiate object type '").Append(type.Name).Append("'. Could not find property '").Append(kvp.Key).Append("'. Has properties ");
                        {
                            bool entry = false;
                            foreach (string key in namedSetProperties.Keys)
                            {
                                if (entry)
                                {
                                    sb.Append(", ");
                                }
                                sb.Append("'").Append(key).Append("'");
                                entry = true;
                            }
                        }
                        sb.Append(". Given properties ");
                        if (properties is IDictionary<string, object>)
                        {
                            bool entry = false;
                            foreach (string key in dict.Keys)
                            {
                                if (entry)
                                {
                                    sb.Append(", ");
                                }
                                sb.Append("'").Append(key).Append("'");
                                entry = true;
                            }
                        }
                        sb.Append(".");
                        throw new InvalidOperationException(sb.ToString());
                    }
                }

                if (remainingRequired.Count > 0)
                {
                    StringBuilder sb = new StringBuilder();
                    sb.Append("Unable to instantiate object type '").Append(type.Name).Append("'. Could not find required properties: ");
                    {
                        bool entry = false;
                        foreach (string key in remainingRequired)
                        {
                            if (entry)
                            {
                                sb.Append(", ");
                            }
                            sb.Append("'").Append(key).Append("'");
                            entry = true;
                        }
                    }
                    sb.Append(".");
                    throw new InvalidOperationException(sb.ToString());                        
                }
                return result;
            }
            else
            {
                throw new NotSupportedException();
            }
        }


        public AmfObjects.NamedDictionary GetNamedDictionary(object p)
        {
            AmfObjects.NamedDictionary result = new AmfObjects.NamedDictionary();
            result.TypeName = TypeHelper.GetContractName(type);

            foreach (KeyValuePair<string, GetValueDelegate> kvp in namedGetProperties)
            {
                object value = kvp.Value(p);

                if (value != null)
                {
                    // ignore self-referencing values
                    if (value.GetType() == p.GetType() && value is IComparable && (value as IComparable).CompareTo(p) == 0)
                    {
                        continue;
                    }
                }

                result.Properties[kvp.Key] = value;
            }

            return result;
        }
    }
}
